/*
  ConfigIP.ino, Example for the AutoConnect library.
  Copyright (c) 2019, Hieromon Ikasamo
  https://github.com/Hieromon/AutoConnect
  This software is released under the MIT License.
  https://opensource.org/licenses/MIT

  This sketch implements an example of enabling a custom web page
  according to the state of an externally equipped switch. The custom
  web page configures and provides a static IP to the ESP module.
  This example needs to equip an external switch circuit that pulls up
  with a resistor (1K to 10K ohms) and then drops it to GND via a push
  switch to connect to any GPIO of the ESP module.

  An external switch circuit:
     3.3V
    --+--
      |
     +-+
     | | 1K ~ 10K
     +-+
      |
      +--> D1 (for ESP8266, ex: GPIO16 in case of ESP32)
      |
    | O
  --+
    | O
      |
    --+--
     GND
*/

#include <SD.h>
#if defined(ARDUINO_ARCH_ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#define EXTERNAL_SWITCH_PIN 5
#elif defined(ARDUINO_ARCH_ESP32)
#include <WiFi.h>
#include <WebServer.h>
using SDClass = fs::SDFS;
#define EXTERNAL_SWITCH_PIN 16
#endif
#define SD_CS_PIN 4
#include <AutoConnect.h>
#include <EEPROM.h>

static const char AUX_CONFIGIP[] PROGMEM = R"(
{
  "title": "Config IP",
  "uri": "/configip",
  "menu": true,
  "element": [
    {
      "name": "caption",
      "type": "ACText",
      "value": "<b>Module IP configuration</b>",
      "style": "color:steelblue",
      "posterior": "div"
    },
    {
      "name": "mac",
      "type": "ACText",
      "format": "MAC: %s",
      "style": "font-size:smaller",
      "posterior": "par"
    },
    {
      "name": "staip",
      "type": "ACInput",
      "label": "IP",
      "pattern": "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",
      "global": true
    },
    {
      "name": "gateway",
      "type": "ACInput",
      "label": "Gateway",
      "pattern": "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",
      "global": true
    },
    {
      "name": "netmask",
      "type": "ACInput",
      "label": "Netmask",
      "pattern": "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",
      "global": true
    },
    {
      "name": "dns1",
      "type": "ACInput",
      "label": "DNS",
      "pattern": "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",
      "global": true
    },
    {
      "name": "ok",
      "type": "ACSubmit",
      "value": "OK",
      "uri": "/restart"
    },
    {
      "name": "cancel",
      "type": "ACSubmit",
      "value": "Cancel",
      "uri": "/_ac"
    }
  ]
}
)";

static const char AUX_RESTART[] PROGMEM = R"(
{
  "title": "Config IP",
  "uri": "/restart",
  "menu": false,
  "element": [
    {
      "name": "caption",
      "type": "ACText",
      "value": "Settings",
      "style": "font-family:Arial;font-weight:bold;text-align:center;margin-bottom:10px;color:steelblue",
      "posterior": "div"
    },
    {
      "name": "staip",
      "type": "ACText",
      "format": "IP: %s",
      "posterior": "br",
      "global": true
    },
    {
      "name": "gateway",
      "type": "ACText",
      "format": "Gateway: %s",
      "posterior": "br",
      "global": true
    },
    {
      "name": "netmask",
      "type": "ACText",
      "format": "Netmask: %s",
      "posterior": "br",
      "global": true
    },
    {
      "name": "dns1",
      "type": "ACText",
      "format": "DNS1: %s",
      "posterior": "br",
      "global": true
    },
    {
      "name": "result",
      "type": "ACText",
      "posterior": "par"
    }
  ]
}
)";

AutoConnect portal;
AutoConnectConfig config;
AutoConnectAux    auxIPConfig;
AutoConnectAux    auxRestart;

// Pin assignment for an external configuration switch
uint8_t ConfigPin = EXTERNAL_SWITCH_PIN;
uint8_t ActiveLevel = LOW;

// EEPROM saving structure
typedef union {
  struct {
    uint32_t  ip;
    uint32_t  gateway;
    uint32_t  netmask;
    uint32_t  dns1;
  } ipconfig;
  uint8_t  ipraw[sizeof(uint32_t) * 4];
} IPCONFIG;

// Load IP configuration from EEPROM
void loadConfig(IPCONFIG* ipconfig) {
  EEPROM.begin(sizeof(IPCONFIG));
  int dp = 0;
  for (uint8_t i = 0; i < 4; i++) {
    for (uint8_t c = 0; c < sizeof(uint32_t); c++)
      ipconfig->ipraw[c + i * sizeof(uint32_t)] = EEPROM.read(dp++);
  }
  EEPROM.end();

  // Unset value screening
  if (ipconfig->ipconfig.ip == 0xffffffffL)
    ipconfig->ipconfig.ip = 0U;
  if (ipconfig->ipconfig.gateway == 0xffffffffL)
    ipconfig->ipconfig.gateway = 0U;
  if (ipconfig->ipconfig.netmask == 0xffffffffL)
    ipconfig->ipconfig.netmask = 0U;
  if (ipconfig->ipconfig.dns1 == 0xffffffffL)
    ipconfig->ipconfig.dns1 = 0U;

  Serial.println("IP configuration loaded");
  Serial.printf("Sta IP :0x%08lx\n", (long unsigned int)ipconfig->ipconfig.ip);
  Serial.printf("Gateway:0x%08lx\n", (long unsigned int)ipconfig->ipconfig.gateway);
  Serial.printf("Netmask:0x%08lx\n", (long unsigned int)ipconfig->ipconfig.netmask);
  Serial.printf("DNS1   :0x%08lx\n", (long unsigned int)ipconfig->ipconfig.dns1);
}

// Save current IP configuration to EEPROM
void saveConfig(const IPCONFIG* ipconfig) {
  // EEPROM.begin will truncate the area to the size given by the argument.
  // The part overflowing from the specified size is filled with 0xff,
  // so if the argument value is too small, the credentials may be lost.
  EEPROM.begin(128);

  int dp = 0;
  for (uint8_t i = 0; i < 4; i++) {
    for (uint8_t d = 0; d < sizeof(uint32_t); d++)
      EEPROM.write(dp++, ipconfig->ipraw[d + i * sizeof(uint32_t)]);
  }
  EEPROM.end();
  delay(100);
}

// Custom web page handler to set current configuration to the page
String getConfig(AutoConnectAux& aux, PageArgument& args) {
  IPCONFIG  ipconfig;
  loadConfig(&ipconfig);

  // Fetch MAC address
  String  macAddress;
  uint8_t mac[6];
  WiFi.macAddress(mac);
  for (uint8_t i = 0; i < 6; i++) {
    char buf[3];
    sprintf(buf, "%02X", mac[i]);
    macAddress += buf;
    if (i < 5)
      macAddress += ':';
  }
  aux["mac"].value = macAddress;

  // Fetch each IP address configuration from EEPROM
  IPAddress staip = IPAddress(ipconfig.ipconfig.ip);
  IPAddress gateway = IPAddress(ipconfig.ipconfig.gateway);
  IPAddress netmask = IPAddress(ipconfig.ipconfig.netmask);
  IPAddress dns1 = IPAddress(ipconfig.ipconfig.dns1);

  // Echo back the IP settings
  aux["staip"].value = staip.toString();
  aux["gateway"].value = gateway.toString();
  aux["netmask"].value = netmask.toString();
  aux["dns1"].value = dns1.toString();

  return String();
}

// Convert IP address from AutoConnectInput string value
void getIPAddress(String ipString, uint32_t* ip) {
  IPAddress ipAddress;

  if (ipString.length())
    ipAddress.fromString(ipString);
  *ip = (uint32_t)ipAddress;
}

// Custom web page handler to save the configuration to AutoConnectConfig
String setConfig(AutoConnectAux& aux, PageArgument& args) {
  IPCONFIG  ipconfig;

  // Retrieve each IP address from AutoConnectInput field
  getIPAddress(aux["staip"].value, &ipconfig.ipconfig.ip);
  getIPAddress(aux["gateway"].value, &ipconfig.ipconfig.gateway);
  getIPAddress(aux["netmask"].value, &ipconfig.ipconfig.netmask);
  getIPAddress(aux["dns1"].value, &ipconfig.ipconfig.dns1);

  // Make a result message
  if (auxIPConfig.isValid()) {
    saveConfig(&ipconfig);
    aux["result"].value = "Reset by AutoConnect menu will restart with the above.";
  }
  else
    aux["result"].value = "Invalid IP address specified.";
  return String();
}

// Sense the external switch to enter the configuraton mode
bool senseSW(const uint8_t pin, const uint8_t activeLevel) {
  bool  sw = digitalRead(pin) == activeLevel;
  if (sw) {
    // Cut-off the chattering noise
    unsigned long tm = millis();
    while (digitalRead(pin) == activeLevel) {
      if (millis() - tm > 1000)
        break;
      delay(1);
    }
  }
  return sw;
}

void setup() {
  delay(1000);
  Serial.begin(115200);
  Serial.println();

  // Shift the credentials storage to reserve saving IPCONFIG
  config.boundaryOffset = sizeof(IPCONFIG);

  // Load current IP configuration
  IPCONFIG  ipconfig;
  loadConfig(&ipconfig);

  // Configure pre-loaded static IPs
  config.staip = IPAddress(ipconfig.ipconfig.ip);
  config.staGateway = IPAddress(ipconfig.ipconfig.gateway);
  config.staNetmask = IPAddress(ipconfig.ipconfig.netmask);
  config.dns1 = IPAddress(ipconfig.ipconfig.dns1);

  /* Prioritizing the IP configuration specified in AutoConnectConfig
  over existing configuration must be accompanied by an explicit
  indication via the preserveIP.
  Background on the need for preserveIP indication:
  - AutoConnect reserves IP configuration in addition to credentials
    to restore the connection. By default, AutoConnect will restore
    the IP configuration saved as part of AutoConnectCredential in
    preference to the IP configuration such as the staip specified
    with AutoConnectConfig.
    The preserveIP setting suppresses restoration from AutoConnectCredential
    to the predefined IP settings with AutoConnectConfig::staip etc.
  */
  config.preserveIP = true;
  portal.config(config);

  /* The restoreCredential is paired with the saveCredential function
  to allow credentials to be restored. The subsequent restoreCredential
  call example instructs recovery from SD.
  If the CS signal of SD peripheral (it is sometimes declared as the
  SS signal in SPI) is different from the default (usually pin #5 on
  ESP32 DevkitC, #4 on M5Stack CORE TF_CARD_CS_PIN etc.),
  you will need to make your own call to SD.begin(YOUR_CS_ASSIGNMENT)
  in the sketch. Then specify false for the 3rd parameter of
  restoreCredentail and saveCredential.
  However, if the start of the file system is delegated to AutoConnect,
  the SD.begin call can be omitted by specifying true for the third parameter.
  */
  SD.begin(SD_CS_PIN);

  portal.restoreCredential<SDClass>("/credt.dat", SD);
  // portal.restoreCredential("/credt.dat");  // For restoring from Flash filesystem

  // Sense the configuration button (external switch)
  pinMode(ConfigPin, INPUT);
  if (senseSW(ConfigPin, ActiveLevel)) {
    Serial.println("IP configuration enable");
    auxIPConfig.load(AUX_CONFIGIP);
    auxIPConfig.on(getConfig);
    auxRestart.load(AUX_RESTART);
    auxRestart.on(setConfig);
    portal.join({ auxIPConfig, auxRestart });
  }

  if (portal.begin()) {
    /* The saveCredential function saves the WiFi credentials currently
    held by AutoConnect in bulk to an external file. The output file
    can be input to the restore function prior to AutoConnect::begin.
    */
    portal.saveCredential<SDClass>("/credt.dat", SD);
    // portal.saveCredential("/credt.dat");  // For saving to Flash filesystem
  }

}

void loop() {
  // User sketch process is here.

  portal.handleClient();
}
